﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using AbstractProducts;

namespace SmartAssembly
{
    public sealed class Report
    {
        private readonly Guid m_Id;
        private readonly ProductRef m_ProductRef;
        private readonly DateTime m_BuildDateUtc;
        private readonly Version m_BuildVersion;
        private readonly string m_ExceptionTypeName;
        private readonly string m_ExceptionMessage;
        private readonly string m_MethodTypeName;
        private readonly string m_MethodName;
        private readonly string m_HumanExceptionTypeName;
        private readonly string m_HumanMethodTypeName;
        private readonly string m_HumanMethodName;
        private readonly string m_SubmitterIpOrGuid;
        private readonly long m_TotalAttachmentSize;
        private readonly Dictionary<string, string> m_CustomProperties;
        private readonly string m_ShortAssemblyNameOfMethodTypeOrNull;
        private readonly Dictionary<string, AssemblyName> m_AssemblyProperNames;
        private readonly ReportedStackTrace m_ReportedStackTrace;

        public Report(Guid id, ProductRef productRef, XDocument document)
        {
            m_Id = id;
            m_ProductRef = productRef;
            XElement element = document.Root;
            if (element == null) throw new InvalidOperationException();

            if (GetAttributeValueOrNull(element, "MapNotAvailable") == "1")
            {
                throw new InvalidOperationException("The map file is not available");
            }

            string buildDateTimeStr = GetAttributeValueOrThrow(element, "BuildDateTime");
            DateTime buildDateLocal = DateTime.Parse(buildDateTimeStr, CultureInfo.InvariantCulture);
            m_BuildDateUtc = buildDateLocal.ToUniversalTime(); // we assume the build server is in our time zone
            string buildVersionStr = GetAttributeValueOrThrow(element, "BuildVersion");
            m_BuildVersion = new Version(buildVersionStr);
            m_ExceptionTypeName = GetAttributeValueOrThrow(element, "ExceptionType");
            m_ExceptionMessage = GetAttributeValueOrThrow(element, "ExceptionMessage");
            m_MethodTypeName = GetAttributeValueOrThrow(element, "TypeName");
            m_MethodName = GetAttributeValueOrThrow(element, "MethodName");
            m_ReportedStackTrace = GetStackTrace(document);
            m_CustomProperties = GetAllCustomProperties(document);
            m_ShortAssemblyNameOfMethodTypeOrNull = ReportUtils.GetAssemblyNameOrNull(m_MethodTypeName);
            m_HumanExceptionTypeName = ReportUtils.GetHumanTypeName(m_ExceptionTypeName);
            m_HumanMethodTypeName = ReportUtils.GetHumanTypeName(m_MethodTypeName);
            m_HumanMethodName = ReportUtils.GetHumanMethodName(m_MethodName);
            m_SubmitterIpOrGuid = GetAttributeValueOrNull(element, "UserHostAddress");
            m_TotalAttachmentSize = GetTotalAttachmentsSize(document);
            m_AssemblyProperNames = GetAssemblyProperNames(document);
        }

        private static string GetAttributeValueOrNull(XElement element, string attributeName)
        {
            XAttribute attribute = element.Attribute(attributeName);
            return attribute == null ? null : attribute.Value;
        }

        private static string GetAttributeValueOrThrow(XElement element, string attributeName)
        {
            XAttribute attribute = element.Attribute(attributeName);
            if (attribute == null)
            {
                throw new ArgumentException(String.Format("element does not contain attribute {0}", attributeName), "element");
            }
            return attribute.Value;
        }

        private static ReportedStackTrace GetStackTrace(XDocument document)
        {
            IList<ReportedException> reportedExceptions = new List<ReportedException>();

            int stackDepthEstimate = 0;
            List<ReportedStackFrame> observedStackFramesForThisException = new List<ReportedStackFrame>();
            Action<string, string> spewFrames =
                (x, y) =>
                    {
                        reportedExceptions.Add(new ReportedException(x, y, observedStackFramesForThisException));

                        stackDepthEstimate = 0;
                        observedStackFramesForThisException = new List<ReportedStackFrame>();
                    };

            string exceptionTypeName = null;
            string exceptionMessage = null;

            XElement stackTraceElement = document.Descendants("StackTrace").Single();
            foreach (XElement element in stackTraceElement.Elements())
            {
                switch (element.Name.LocalName)
                {
                    case "Exception":
                        if (exceptionTypeName != null)
                        {
                            if (exceptionMessage == null)
                            {
                                throw new InvalidOperationException();
                            }
                            spewFrames(exceptionTypeName, exceptionMessage);
                        }
                        exceptionTypeName = GetAttributeValueOrThrow(element, "Type");
                        exceptionMessage = GetAttributeValueOrThrow(element, "Message");

                        string exceptionStackTrace = GetAttributeValueOrNull(element, "ExceptionStackTrace");
                        if (exceptionStackTrace != null)
                        {
                            // so that we don't suffer from SA-269
                            string decodedStackTrace = SDK.Helpers.DecodeStackTrace(exceptionStackTrace);

                            // Add all lines from the .NET stack trace. We'll replace these ReportedStackFrames with better ones later if possible.
                            string[] decodedStackTraceLines = decodedStackTrace.Split(new[] { "&#xD;&#xA;", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
                            observedStackFramesForThisException.AddRange(decodedStackTraceLines.Select(line => new ReportedStackFrame(line, null, null, null, null)));
                        }
                        break;

                    case "StackFrame":
                        XAttribute exceptionStackDepth = element.Attribute("ExceptionStackDepth");
                        if (exceptionStackDepth != null)
                        {
                            stackDepthEstimate = int.Parse(exceptionStackDepth.Value) - 1;
                        }

                        string type = GetAttributeValueOrNull(element, "Type");
                        string method = GetAttributeValueOrNull(element, "Method");
                        string documentUrl = GetAttributeValueOrNull(element, "DocumentURL");
                        string lineNumberStr = GetAttributeValueOrNull(element, "LineNumber");
                        int? lineNumber = lineNumberStr == null ? null : (int?)Int32.Parse(lineNumberStr);

                        //this makes us basically crash-free, but who knows what'll happen
                        if (stackDepthEstimate >= 0 && stackDepthEstimate < observedStackFramesForThisException.Count)
                        {
                            ReportedStackFrame old = observedStackFramesForThisException[stackDepthEstimate];
                            ReportedStackFrame reportedStackFrame = new ReportedStackFrame(old.OriginalStackTraceLineOrNull, type, method, documentUrl, lineNumber);
                            observedStackFramesForThisException[stackDepthEstimate] = reportedStackFrame;
                            ++stackDepthEstimate;
                        }
                        else if (stackDepthEstimate == observedStackFramesForThisException.Count)
                        {
                            ReportedStackFrame reportedStackFrame = new ReportedStackFrame(null, type, method, documentUrl, lineNumber);
                            observedStackFramesForThisException.Add(reportedStackFrame);
                            ++stackDepthEstimate;
                        }
                        break;

                    default:
                        throw new NotSupportedException();
                }
            }

            if (exceptionTypeName != null)
            {
                if (exceptionMessage == null)
                {
                    throw new InvalidOperationException();
                }
                spewFrames(exceptionTypeName, exceptionMessage);
            }

            return new ReportedStackTrace(reportedExceptions);
        }

        private static Dictionary<string, string> GetAllCustomProperties(XDocument document)
        {
            Dictionary<string, string> properties = new Dictionary<string, string>();

            XElement customPropertiesElement = document.Descendants("CustomProperties").Single();
            foreach (XElement eachCustomProperty in customPropertiesElement.Elements())
            {
                if (eachCustomProperty.Name.LocalName == "CustomProperty")
                {
                    XAttribute nameAttribute = eachCustomProperty.Attribute("Name");
                    if (nameAttribute == null)
                    {
                        continue;
                    }

                    string valueWithoutStupidQuotesWtfAreTheyThereFor;

                    XAttribute nullAttribute = eachCustomProperty.Attribute("Null");
                    if (nullAttribute != null && nullAttribute.Value == "1")
                    {
                        valueWithoutStupidQuotesWtfAreTheyThereFor = null;
                    }
                    else
                    {
                        XAttribute valueAttribute = eachCustomProperty.Attribute("Value");
                        if (valueAttribute == null)
                        {
                            valueWithoutStupidQuotesWtfAreTheyThereFor = null;
                        }
                        else
                        {
                            string value = valueAttribute.Value;
                            valueWithoutStupidQuotesWtfAreTheyThereFor = value.Substring(1, value.Length - 2);
                        }
                    }
                    properties[nameAttribute.Value] = valueWithoutStupidQuotesWtfAreTheyThereFor;
                }
            }

            return properties;
        }

        private static long GetTotalAttachmentsSize(XDocument document)
        {
            XElement attachedFilesElement = document.Descendants("AttachedFiles").SingleOrDefault();

            if (attachedFilesElement == null)
            {
                return 0;
            }

            return (attachedFilesElement.Elements().Where(
                eachAttachedFile => eachAttachedFile.Name.LocalName == "AttachedFile").Select(
                    eachAttachedFile => long.Parse(GetAttributeValueOrThrow(eachAttachedFile, "Length")))).Sum();
        }

        public IEnumerable<KeyValuePair<string, string>> GetCustomProperties()
        {
            return m_CustomProperties;
        }

        private static Dictionary<string, AssemblyName> GetAssemblyProperNames(XDocument document)
        {
            Dictionary<string, AssemblyName> assemblyNames = new Dictionary<string, AssemblyName>();

            XElement assembliesElement = document.Descendants("Assemblies").Single();


            foreach (AssemblyName name in from XElement eachAssembly in assembliesElement.Elements()
                                          where eachAssembly.Name.LocalName == "Assembly"
                                          select new AssemblyName(GetAttributeValueOrThrow(eachAssembly, "Name")))
            {
                assemblyNames[name.Name] = name;
            }

            return assemblyNames;
        }

        public string GetCustomProperty(string name)
        {
            return m_CustomProperties.ContainsKey(name) ? m_CustomProperties[name] : null;
        }

        public Guid Id
        {
            get { return m_Id; }
        }

        /// <summary>
        /// This is the unchanged <see cref="ProductRef"/>.  Are you sure you don't want <see cref="ProductBuildRef.ProductRef"/> instead?
        /// </summary>
        public ProductRef OriginalUnchangedReportProductRef
        {
            get { return m_ProductRef; }
        }

        public DateTime BuildDateUtc
        {
            get { return m_BuildDateUtc; }
        }

        /// <summary>
        /// This is internal, because you should use <see cref="ProcessedReport.ProductBuildRef"/> and then <see cref="ProductBuildRef.ProductBuildVersion"/> instead
        /// </summary>
        internal Version BuildVersion
        {
            get { return m_BuildVersion; }
        }

        public string ExceptionTypeName
        {
            get { return m_ExceptionTypeName; }
        }

        public string ExceptionMessage
        {
            get { return m_ExceptionMessage; }
        }

        public string MethodTypeName
        {
            get { return m_MethodTypeName; }
        }

        public ReportedStackTrace ReportedStackTrace
        {
            get { return m_ReportedStackTrace; }
        }

        public string MethodName
        {
            get { return m_MethodName; }
        }

        public string HumanExceptionTypeName
        {
            get { return m_HumanExceptionTypeName; }
        }

        public string HumanMethodTypeName
        {
            get { return m_HumanMethodTypeName; }
        }

        public string HumanMethodName
        {
            get { return m_HumanMethodName; }
        }

        public string SubmitterIpOrGuid
        {
            get { return m_SubmitterIpOrGuid; }
        }

        public long TotalAttachmentSize
        {
            get { return m_TotalAttachmentSize; }
        }

        public string ShortAssemblyNameOfMethodTypeOrNull
        {
            get { return m_ShortAssemblyNameOfMethodTypeOrNull; }
        }

        public AssemblyName GetProperAssemblyName(string shortAssemblyName)
        {
            return m_AssemblyProperNames[shortAssemblyName];
        }
    }
}
